#-*- coding:utf-8 -*-

from functools import wraps
import cv2
import math
import numpy as np
import os
try:
    import cPickle as pickle
except ImportError:
    import pickle

face_cascade = cv2.CascadeClassifier('./cascades/haarcascade_frontalface_alt.xml')
eye_cascade = cv2.CascadeClassifier('./cascades/haarcascade_eye_tree_eyeglasses.xml')
nose_cascade = cv2.CascadeClassifier('./cascades/haarcascade_mcs_nose.xml')
mouth_cascade = cv2.CascadeClassifier('./cascades/haarcascade_mcs_mouth.xml')

class _const(object):
    class ConstError(TypeError): pass
    def __setattr__(self, name, value):
        if name in self.__dict__:
            raise self.ConstError, "Can't rebind const(%s)" %name
        self.__dict__[name] = value

    def __delattr__(self, name):
        if name in self.__dict__:
            raise self.ConstError, "Can't unbind const(%s)" %name
        raise NameError, name

# exceptions
class ReadException(IOError):
    pass

class ImageNotFound(IOError):
    pass

class DetectExcepetion(Exception):
    pass

class UnExpectedValueException(ValueError):
    pass

# index:
# [0]: begin x
# [1]: begin y
# [2]: span x
# [3]: span y
def _mid_x(arr):
    return (2*arr[0]+arr[2])/2

def _mid_y(arr):
    return (2*arr[1]+arr[3])/2

def enlargeimage(img, scale):
    '''
    enlarge image by given scale.
    '''
    height,width = img.shape[:2]
    beginH = int(round(height*scale))
    endH = int(round(height-beginH))
    beginW = int(round(width*scale))
    endW = int(round(width-beginW))
    if beginH < endH and beginW < endW:
        new_img = img[beginH:endH,beginW:endW]
    else:
        new_img = img[endH:beginH,endW:beginW]
    return new_img


def clahe(img):
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
    cl = clahe.apply(img)
    return cl

def shape(img):
    '''
    Returns height and width of given image
    '''
    assert img != None
    height, width = img.shape[:2]
    return height, width

def drawlines(img, h_lines, v_lines,
        color=(0,0,0),px=1):
    '''
    enlarge image by given scale.
    '''
    vis = img.copy()
    height,width = vis.shape[:2]
    offheight,offwidth = float(height)/v_lines, float(width)/h_lines
    font = cv2.FONT_HERSHEY_PLAIN

    for i in range(1,v_lines+1):
        cv2.line(vis,(0,int(i*offheight)),(width,int(i*offheight)),color,px)
    for i in range(1,h_lines+1):
        cv2.line(vis,(int(i*offwidth),0),(int(i*offwidth),height),color,px)
    return vis

def merge_image(imglist):
    '''
    merge images, automatically set the size accroding to the 
    shape of first element in the list.

    Parameters: 
        imglist : a list including images to merge.

    Returns:    
        vis : canvas of merged images.
    '''    
    assert imglist != None and len(imglist) > 1
    first = imglist[0]
    h, w = first.shape[:2]
    for img in imglist:
        img = cv2.resize(img,(w,h),interpolation=cv2.INTER_LINEAR)
    vis = np.concatenate(tuple(imglist), axis=1)
    return vis

def cut_img(face, start_x, end_x, start_y, end_y):
    '''
    cut a part of image by given position.

    Parameters: 
        face : the specific image of face.
        start_x : begin position of Horizental Axis.
        end_x : end position of Horizental Axis.
        start_y : begin position of Vertical Axis.
        end_y : end position of Vertical Axis.

    Returns:    
        cut_img : processed image.
    '''
    assert face != None
    height, width = face.shape[:2]
    start_x = int(start_x*width)
    end_x = int(end_x*width)
    start_y = int(start_y*height)
    end_y = int(end_y*height)
    
    cut_img = np.zeros((end_y-start_y,end_x-start_x),np.uint8)
    cut_img[:] = face[start_y:end_y, start_x:end_x]
    return cut_img

def roi_grayvalue(face, start_x, end_x, start_y, end_y, rounds=None):
    '''
    Compute the gray value of roi.

    '''
    assert face != None
    roi_img = cut_img(face, start_x, end_x, start_y, end_y)
    value = gray_value(roi_img)
    return value if not rounds else round(value, 5)

def showplt(img):
    '''
    show histogram of image.
    '''
    from matplotlib.font_manager import FontProperties
    font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)
    hist_full = cv2.calcHist([img],[0],None,[256],[0,256])
    plt.subplot(211),plt.imshow(img,'gray'),plt.title(u'结果', fontproperties=font)
    plt.subplot(212),plt.plot(hist_full,'gray'),plt.xlim([0,256])
    plt.xlabel(u'灰度范围', fontproperties=font)
    plt.ylabel(u'像素数量', fontproperties=font)
    plt.show()

def find_eyes(face):
    '''
    Return positions of left eye and right eye on the face.

    Parameters: 
        face : specific image
    Returns:    
        pos_tuple: pos_tuple of eyes, each element of tuple is a tuple of position,
                   the first element is the position of Horizental Axis (X),
                   the second element is the position of Vertical Axis (Y).

    '''
    assert face != None
    eyepos = []
    height, width = face.shape[:2]

    eyes = get_organs(face).get_eyes()

    if len(eyes) != 0:
        for (ex,ey,ew,eh) in eyes:
            eyepos.append((ex+ew/2,ey+eh/2))

    if len(eyepos)==2:
        eyepos = sorted(eyepos,key=lambda eye:eye[0])
        leye = eyepos[0]
        reye = eyepos[1]
        pos_tuple = (leye, reye)
        return pos_tuple

    return None


def adjustface(face, 
            find=False, zoom_factor=0):
    '''
    Correct angle of face.

    Parameters: 
        face : the specific image of face.
        find : whether to open face cascade.
        zoom_factor: if this mode is opened, it is meant to be the 
                     proportion of eye's distance account for image's width.
    Returns:    
        real : processed image after adjustment.
        face : origin image
    '''
    assert face != None

    gray = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY)

    height,width = face.shape[:2]

    if find == True:
        face_ = face_cascade.detectMultiScale(face)
        if len(face_) != 0:
            face_ = sorted(face_, key=lambda item:item[2])[-1]
            face = face[face_[0]:face_[0]+face_[2], face_[1]:face_[1]+face_[2]]

    eyepos = find_eyes(face)

    if eyepos:
        leye = eyepos[0]
        reye = eyepos[1]

        yy = math.fabs(leye[1] - reye[1])
        xx = math.fabs(leye[0] - reye[0])
        degree = math.degrees(math.atan(yy/xx))

        if degree > 5:
            degree = degree if reye[1] > leye[1] else -degree
            M = cv2.getRotationMatrix2D((width/2,height/2), degree, 1)
        else:
            M = cv2.getRotationMatrix2D((width/2,height/2),0,1)

        face = cv2.warpAffine(face,M,(width,height))
        scale = 0.0

        if zoom_factor > 0:
            distance = leye[0]-reye[0] if leye[0] > reye[0] else reye[0] - leye[0]
            proportion = float(distance)/width
            scale = (width-distance/zoom_factor)/(2*width)
            face = enlargeimage(face,scale)
        return face, scale
    else:
        return face, None

def gray_to_pseudo(gray_val):
    '''
    Return RGB of pseudo color converted by gray value.

    Returns a tuple of RGB.
    Parameters: 
        gray_val : specific gray value
    Returns:    
        (R,G,B): a tuple of R, G, B

    '''
    if gray_val > 255 or gray_val < 0:
        raise UnExpectedValueException("错误的灰度范围")

    R = G = B = None
    c = gray_val / 64
    if c == 0:
        R = 0
        G = gray_val * 4
        B = 255
    elif c == 1:
        R = 0
        G = 255
        B = 511 - gray_val * 4
    elif c == 2:
        R = gray_val * 4 - 511
        G = 255
        B = 0
    elif c == 3:
        R = 255
        G = 1023 - gray_val * 4
        B = 0
    else:
        # impossible to here
        pass

    return (R,G,B) if R is not None and G is not None and B is not None else None

def pick_skin(face, skinMask=None):
    '''
    Return a image of skin area along with the mask.

    Parameters: 
        face : the specific image.
    Returns:    
        skin : image split by skin

    '''
    if skinMask is None:
        lower = np.array([0, 48, 80], dtype = "uint8")
        upper = np.array([20, 255, 255], dtype = "uint8")

        converted = cv2.cvtColor(face, cv2.COLOR_BGR2HSV)
        skinMask = cv2.inRange(converted, lower, upper)
     
        # apply a series of erosions and dilations to the mask
        # using an elliptical kernel
        kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11))
        skinMask = cv2.erode(skinMask, kernel, iterations = 2)
        skinMask = cv2.dilate(skinMask, kernel, iterations = 2)
     
        # blur the mask to help remove noise, then apply the
        # mask to the face
        skinMask = cv2.GaussianBlur(skinMask, (3, 3), 0)
        
    skin = cv2.bitwise_and(face, face, mask = skinMask)
    
    # show the skin in the image along with the mask
    # cv2.imshow("images", np.hstack([face, skin]))

    return skin, skinMask

def read(path, 
        gray=False):
    '''
    Return handle of image opened by cv2.imread().

    Parameters: 
        path : path of specific image.
        gray : use 'gray' mode to open the image.
    Returns:    
        img : a handle of image.

    '''
    if gray:
        img = cv2.imread(path, 0)
    else:
        img = cv2.imread(path)
    if img == None:
        raise ImageNotFound("图片不存在")
    return img

def write(dst, img):
    '''
    Write a image on some path
    '''
    cv2.imwrite(dst, img)

def convert(img, mode=cv2.COLOR_BGR2GRAY):
    '''
    Convert image color with the mode.

    Parameters:
        img : input image.
        mode : convert mode.
            such as:
                cv2.COLOR_BGR2GRAY
                cv2.COLOR_BGR2HSV
    '''
    vis = cv2.cvtColor(img, mode)
    if vis is not None:
        return vis
    else:
        return img

def show(img):
    '''
    Show a image, press ESC to exist.
    '''
    while True:
        cv2.imshow("img", img)
        if cv2.waitKey(0) == 27:
            break
    cv2.destroyAllWindows()

def gray_value(gray, 
            extract=False):
    '''
    Return average gray value of an image.

    Parameters: 
        face : the specific grayscale image.
        extract : if this option is open, the image array will remove all zero elements.
    Returns:    
        value : gray value.

    '''
    assert gray != None
    if extract:
        gray = gray[gray!=0]
    return gray.mean()

def gray_value_filter(gray, low_bound, up_bound):
    assert up_bound > low_bound
    gray = gray[(gray>low_bound)&(gray<up_bound)]
    return gray.mean()


def grab_cut(face):
    '''
    Returns foreground of image using Grab-Cut algorithm.
    '''
    assert face != None
    height,width = face.shape[:2]

    mask = np.zeros(face.shape[:2], np.uint8)
    bgdModel = np.zeros((1,65), np.float64)
    fgdModel = np.zeros((1,65), np.float64)
    rect = get_organs(face).get_face()
    cv2.grabCut(face, mask, rect, bgdModel, fgdModel,5, cv2.GC_INIT_WITH_RECT)
    mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
    face = face*mask2[:,:,np.newaxis]
    
    (_, face) = cv2.threshold(face, 50, 255, cv2.THRESH_TOZERO)

    face = np.ma.masked_equal(face,0)

    return face

def _memory_study(function):
    @wraps(function)
    def __memory_study(*args, **kw):
        result = function(*args,**kw)
        linesArr, status = result
        if status is False:
            return result
        try:
            STUDY_FILE = "memory.pkl"
            if os.path.exists(STUDY_FILE) == False:
                with open(STUDY_FILE,'wb') as pkl:
                    pickle.dump((linesArr,1),pkl)
            else:
                with open(STUDY_FILE,'rb') as memory_file:
                    m_linesArr,count = pickle.load(memory_file)
                    VLines = sorted([i for i in linesArr if i['y']==0],key=lambda x:x['x'])
                    HLines = sorted([i for i in linesArr if i['x']==0],key=lambda x:x['y'])
                    m_VLines = sorted([i for i in m_linesArr if i['y']==0],key=lambda x:x['x'])
                    m_HLines = sorted([i for i in m_linesArr if i['x']==0],key=lambda x:x['y'])
                for now, memory in zip(VLines, m_VLines):
                    memory['x'] = (now['x']+memory['x']*count)/(count+1)
                for now, memory in zip(HLines, m_HLines):
                    memory['y'] = (now['y']+memory['y']*count)/(count+1)
                m_linesArr = m_VLines + m_HLines
                with open(STUDY_FILE,'wb') as memory_file:
                    pickle.dump((m_linesArr,count+1), memory_file)
        finally:
            return result
    return __memory_study

def get_organs(face):
    height, width = face.shape[:2]

    def _f():
        pass

    '''
    return an array which includes the location of face
        [start_x, start_y, end_x, end_y]
    '''
    def get_face():
        face_ = face_cascade.detectMultiScale(face, 1.3, 5)
        face = [f for f in faces if f[2]>width/3 and f[3]>height/3]

        if len(face) == 1:
            for (x,y,w,h) in faces:
                rect = (x,y,w,h)
        else:
            rect = (width/4, height/4, width*3/width, height*3/4)
        return rect

    '''
    return multi-array with the location of mouths
    '''
    def get_mouths():
        mouths = []
        mouths = mouth_cascade.detectMultiScale(face)
        mouths = [mouth for mouth in mouths if width/3 < _mid_x(mouth) < width*2/3
                                    and mouth[1] > height/2]
        mouths = sorted(mouths,key=lambda mouth:mouth[1])[-1:]
        return mouths

    '''
    return multi-array with the location of noses
    '''
    def get_noses():
        noses = []
        noses = nose_cascade.detectMultiScale(face)
        noses = [nose for nose in noses if width*2/3 > _mid_x(nose) > width*1/3 \
                            and width/3 > nose[2] > width/8 \
                            and height/4 < _mid_y(nose) < height*3/4]
        return noses

    '''
    return multi-array with the location of eyes
    '''
    def get_eyes():
        eyes = []
        eyes = eye_cascade.detectMultiScale(face)
        eyes = [eye for eye in eyes if eye[1] > height/4 \
                                    and height/4 < _mid_y(eye) < height*2/3]
        eyes = sorted(eyes,key=lambda eye:eye[1])[:2]
        eyes = sorted(eyes,key=lambda eye:eye[0])
        return eyes

    _f.get_face = get_face
    _f.get_mouths = get_mouths
    _f.get_noses = get_noses
    _f.get_eyes = get_eyes

    return _f

@_memory_study
def markdetect(face):
    '''
    get face's features
    '''
    C = _const()
    C.BROW_UP = 0.3
    C.BROW_DN = 0.35

    C.EYE_UP = 0.4
    C.EYE_DN = 0.45
    C.EYE_RT = 0.65

    C.NOSE_CT = 0.6
    C.NOSE_DN = 0.66
    C.NOSE_CL = 0.45
    C.NOSE_CR = 0.52
    C.NOSE_RT = 0.54
    C.NOSE_EXRT = 0.6

    C.MOUTH_UP = 0.72
    C.MOUTH_DN = 0.80

    DARWEIGHT = 1

    detect_status = True

    linesArr = []

    real_height, real_width = face.shape[:2]

    organs = get_organs(face)

    noses = organs.get_noses()
    noses_count = len(noses)

    mouths = organs.get_mouths()
    mouths_count = len(mouths)

    eyes = organs.get_eyes()
    eyes_count = len(eyes)

    print "noses:",noses_count, "mouth:",mouths_count, "eyes:",eyes_count

    if eyes_count == 2:
        for idx,(ex,ey,ew,eh) in enumerate(eyes):
            # cv2.rectangle(face,(ex,ey),(ex+ew,ey+eh),(0,255,0),DARWEIGHT)
            if idx == 0:
                linesArr.append(dict(x=0,y=round((ey-eh/4)/float(real_height),3)))
                linesArr.append(dict(x=0,y=round((ey)/float(real_height),3)))

        else:
            linesArr.append(dict(x=0,y=round((ey+eh*3/4)/float(real_height),3)))
            linesArr.append(dict(x=0,y=round((ey+eh/4)/float(real_height),3)))
            linesArr.append(dict(x=round((ex+ew*3/4)/float(real_width),3),y=0))
            linesArr.append(dict(x=round((ex+ew*3/4+real_width/7)/float(real_width),3),y=0))

    else:
        detect_status = False

        linesArr.append(dict(x=0,y=C.BROW_UP))
        linesArr.append(dict(x=0,y=C.BROW_DN))
 
        linesArr.append(dict(x=0,y=C.EYE_UP))
        linesArr.append(dict(x=0,y=C.EYE_DN))
        linesArr.append(dict(x=C.EYE_RT,y=0))
        linesArr.append(dict(x=C.EYE_RT+1.0/7,y=0))


    if len(noses) == 1:
        for(nx,ny,nw,nh) in noses:
            linesArr.append(dict(x=round((nx+nw*1/3)/float(real_width),3),y=0))
            linesArr.append(dict(x=round((nx+nw*2/3)/float(real_width),3),y=0))
            linesArr.append(dict(x=round((nx+nw*3/4)/float(real_width),3),y=0))
            linesArr.append(dict(x=round((nx+nw)/float(real_width),3),y=0))

            linesArr.append(dict(x=0,y=round((ny+nh/4)/float(real_height),3)))
            linesArr.append(dict(x=0,y=round((ny+nh*3/4)/float(real_height),3)))

    else:
        detect_status = False

        linesArr.append(dict(x=C.NOSE_CL,y=0))
        linesArr.append(dict(x=C.NOSE_CR,y=0))
        linesArr.append(dict(x=C.NOSE_RT,y=0))
        linesArr.append(dict(x=C.NOSE_EXRT,y=0))

        linesArr.append(dict(x=0,y=C.NOSE_CT))
        linesArr.append(dict(x=0,y=C.NOSE_DN))

    if len(mouths) == 1:
        for(mx,my,mw,mh) in mouths:
            linesArr.append(dict(x=0,y=round(my/float(real_height),3)))
            linesArr.append(dict(x=0,y=round((my+mh*3/5)/float(real_height),3)))

    else:
        detect_status = False

        linesArr.append(dict(x=0,y=C.MOUTH_UP))
        linesArr.append(dict(x=0,y=C.MOUTH_DN))

    return linesArr, detect_status
